package com.oracle.coherence.patterns.security.management;

import com.oracle.coherence.patterns.security.KrbSecurityHelper;
import com.tangosol.net.management.MBeanConnector;

import javax.management.MBeanServer;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.rmi.RMIConnectorServer;
import javax.rmi.ssl.SslRMIClientSocketFactory;
import javax.rmi.ssl.SslRMIServerSocketFactory;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;

/**
 * This class extends <code>com.tangosol.net.management.MBeanConnector</code>
 * by providing a secure JMX environment.<p/>
 * <p/>
 * It would appear the the Coherence MBeanConnector does not offer the
 * ability to properly secure the JMX connections to the management node. This
 * class allows the option to use an SSL connection with corresponding
 * keystores and also allows a password and access file to be specified.</p>
 * <p/>
 * <p/>
 * <b>NOTE:</b> This implementation does not allow the HTTP MBean server to be started. The
 * HTTP server is not as secure as the RMI server. Regardless of which parameters are passed
 * to the main method, -rmi or -http, the RMI server is always started. Unlike the Coherence
 * MBeanConnector, this class may be run with no command line arguments.<p>
 * <p/>
 * <p/>
 * Default JMX URL service:jmx:rmi://localhost:3000/jndi/rmi://localhost:9000/server
 *
 * @author Jonathan Knight
 * @see MBeanConnector
 */
public class SecureMBeanConnector extends MBeanConnector {
    public static final String JMX_SSL = "oracle.jmx.ssl";
    public static final String JMX_KRB = "oracle.jmx.krb";
    public static final String TRUE = Boolean.TRUE.toString();
    public static final String FALSE = Boolean.FALSE.toString();

    /**
     * Start a Coherence cluster node that enables JMX agents such as Jrockit Mission Control or JConsole
     * to connect to the in-process JMX MBeanServer and manage Coherence MBeans.
     * <p/>
     * Command line usage:
     * <p/>
     * <code>java com.tangosol.net.management.SecureMBeanConnector</code>
     * <p/>
     * Use the following command to connect via JConsole:
     * <p/>
     * <code>jconsole service:jmx:rmi://[host]:[RMI Connection port]/jndi/rmi://[host]:[RMI Registry port]/server</code>
     *
     * @param asArg - the program aruments - these will be ignored the rmi server will be started by default, the
     *              http server is disabled.
     */
    public static void main(String[] asArg) throws Exception {
        String moduleName = System.getProperty("coherence.security.loginmodule", "Coherence");
        LoginContext lc = new LoginContext(moduleName);
        lc.login();
        Subject subject = lc.getSubject();
        Subject.doAs(subject, new PrivilegedExceptionAction<Object>() {
            @Override
            public Object run() throws Exception {
                new SecureMBeanConnector().start(true, false);
                return null;
            }
        });
    }

    /**
     * Overrides the startRmiConnector in the super class and instead
     * starts the RMI connector in a more secure mode.<p/>
     * The RMI connector will use an authenticator that expects Kerberos credentials
     * to be passed to it. Any connection attempt that does not provide these
     * credentials will be rejected.<p/>
     *
     * @return a JMXConnectorServer that has been started
     * @throws IOException
     * @see com.tangosol.net.management.MBeanConnector#startRmiConnector()
     */
    @Override
    public JMXConnectorServer startRmiConnector() throws IOException {
        int nRegPort = getRegistryPort();
        int nConPort = getConnectionPort();
        String sHost = getHost();

        MBeanServer mbs = findMBeanServer();

        HashMap<String, Object> env = createEnvironment();

        JMXConnectorServer connectorServer = startRmiConnector(sHost, nRegPort, nConPort, mbs, env);
        log(connectorServer.getAddress());
        return connectorServer;
    }

    /**
     * Get the port number to use for the RMI registry port.<p/>
     * This value comes from the System property
     * <code>tangosol.coherence.management.remote.registryport</code>.
     * If this property is not present the default value of 9000 will be used.
     * If the property is set, but is not a valid number a NumberFormatException
     * will be thrown.
     *
     * @return the port number to use for the connection port.
     * @throws NumberFormatException if the <code>tangosol.coherence.management.remote.registryport</code>
     *                               is not set to a valid number.
     */
    int getRegistryPort() {
        int registryPort;
        String sRegPort = System.getProperty(RMI_REGISTRY_PORT_PROPERTY);

        try {
            registryPort = (sRegPort == null) ? 9000 : Integer.parseInt(sRegPort);
        }
        catch (NumberFormatException e) {
            throw ensureRuntimeException(e, "Illegal " + RMI_REGISTRY_PORT_PROPERTY + " system property value " + sRegPort);
        }

        return registryPort;
    }

    /**
     * Get the port number to use for the connection port.<p/>
     * This value comes from the System property
     * <code>tangosol.coherence.management.remote.connectionport</code>.
     * If this property is not present the default value of 3000 will be used.
     * If the property is set, but is not a valid number a NumberFormatException
     * will be thrown.
     *
     * @return the port number to use for the connection port.
     * @throws NumberFormatException if the <code>tangosol.coherence.management.remote.connectionport</code>
     *                               is not set to a valid number.
     */
    int getConnectionPort() {
        int connectionPort;
        String sConPort = System.getProperty(RMI_CONNECTION_PORT_PROPERTY);

        try {
            connectionPort = (sConPort == null) ? 3000 : Integer.parseInt(sConPort);
        }
        catch (NumberFormatException e) {
            throw ensureRuntimeException(e, "Illegal " + RMI_CONNECTION_PORT_PROPERTY + " system property value " + sConPort);
        }

        return connectionPort;
    }

    /**
     * Get the Host that this connector should bind to.<p/>
     * This value is obtained from the <code>tangosol.coherence.management.remote.host</code>
     * System property, or defaults to localhost if the property is not set.
     *
     * @return the Host that this connector should bind to
     * @throws UnknownHostException if no IP address is available for local host.
     */
    String getHost() throws UnknownHostException {
        String defaultHost = InetAddress.getLocalHost().getHostName();
        return System.getProperty(RMI_HOST_PROPERTY, defaultHost);
    }

    /**
     * Create the environment map that will be used to configure the JMX
     * management environment.<p/>
     *
     * @return the environment map that will be used to configure the JMX
     *         management environment.
     */
    HashMap<String, Object> createEnvironment() {
        HashMap<String, Object> env = new HashMap<String, Object>();

        boolean useSSL = Boolean.parseBoolean(System.getProperty(JMX_SSL, FALSE));
        if (useSSL) {
            SslRMIClientSocketFactory csf = new SslRMIClientSocketFactory();
            SslRMIServerSocketFactory ssf = new SslRMIServerSocketFactory();
            env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, csf);
            env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, ssf);
        }

        boolean useKrb = Boolean.parseBoolean(System.getProperty(JMX_KRB, TRUE));
        if (useKrb) {
            env.put(JMXConnectorServer.AUTHENTICATOR, new BaseJMXAuthenticator());
        }

        return env;
    }

}
